查看原文
其他

C# 8.0 两个有趣的新特性以及gRPC

DotNet 2020-09-12

(给DotNet加星标,提升.Net技能


转自:雨少主
zhuanlan.zhihu.com/p/63779162

关于C#语法特性的部分需要Visual Studio 2019支持。


关于.NET Core的部分需要安装.NET 3.0 Preview4,低版本或许也可以但我没实验。


如果要在最新版的VS2019中使用.NET 3.0,可能需要在 选项 - 解决方案与项目- ASP.NET Core 中启用 使用 .NET Core SDK 预览版 选项。


C# 8.0新特性:可空的引用类型


static void Main(string[] args)
{
#nullable enable
string a = null;
string? b = null;
var c = a.Length;
var d = b.Length;
var e = b!.Length;
#nullable disable
string? f = null;
}


复制以上简单的代码到IDE就能展现这个特性的特点与用法:


  • IDE会对 a 赋值为 null 的操作进行警告, 因为在约定中 a 不可为空,而 b 则不会警告,因为它可以为 null ;


  • IDE会对 a.Length 的访问进行警告,因为已经静态推断出 a 为 null 了;


  • IDE会对 b.Length 的访问进行警告,b 类型可能为空;


  • b!.Length 的访问操作不会被警告,因为这种形式的访问表示老子已经知道它可能为 null 了你闭嘴;


  • string? f =null 语句会被IDE警告,因为上面已经把可为空的引用类型特性关闭了。


另外此特性不止支持 enable 和 disable 选项,还支持 restore 还原之前的设置,以及通过 safeonly 或 warnings 设置“定制”启用警告的范围,具体可参照其 详细说明 。


我们可以发现这个特性的的实质其实是一个“柔性”断言,启用后IDE会对部分代码进行警告提示,督促我们进行处理,但也止于此了。它非常灵活,新项目启用此特性是值得的,但旧项目也没必要升级。


C# 8.0新特性:using 声明


这里可以直接看官网的例子:


static void WriteLinesToFile(IEnumerable<string> lines)
{
using var file = new System.IO.StreamWriter("WriteLines2.txt");
foreach (string line in lines)
{
// If the line doesn't contain the word 'Second', write the line to the file.
if (!line.Contains("Second"))
{
file.WriteLine(line);
}
}
// file is disposed here
}


等价于:


static void WriteLinesToFile(IEnumerable<string> lines)
{
using (var file = new System.IO.StreamWriter("WriteLines2.txt"))
{
foreach (string line in lines)
{
// If the line doesn't contain the word 'Second', write the line to the file.
if (!line.Contains("Second"))
{
file.WriteLine(line);
}
}
} // file is disposed here
}


也就是说使用 using 关键字修饰的变量声明,它在作用域结束后会自动释放。一开始我没明白这个有什么意义,今天和 @EanCuznaivy谈到某种情况,就是某些类型之所以会继承 IDispose 接口,可能是基于对语义或设计实现上的软需求,并非它一定需要调用 Dispose 方法才能够释放

(比如 ProcessModule Class (System.Diagnostics) )。


在这种情况下,对于我这样的强迫症患者而言,明知道没必要,但也得不厌其烦地 try finally 或者 using{}。有了这个特性,在写类似的代码的时候,可以只多加几个字就让心情舒畅,是强迫症患者的福音。


另外在进行一些很常见的操作比如IO(Stream)、摘要计算(HashAlgorithm)时,可以少写一些代码。


ASP dot NET Core 3.0中的 gRPC 服务


.NET CORE使用gRPC服务需要用到两个Nuget包:


  • 运行时:Google.Protobuf


  • 支持套件:Grpc.Tools


对于客户端而言,还需要 Grpc.Core 包的支持。


Google.Protobuf 不必解释,Grpc.Core 是一系列客户端要用到的API,而 Grpc.Tools 的牛逼之处在于不用编译 *.proto 文件即可直接在C#中引用它……


对于.NET Core 2.1 或 2.2而言使用 gPRC 服务还需要手写微量代码(XXX.BindService方法),而到了.NET CORE 3.0,引用 Grpc.AspNetCore.Server 包后即可直接以惯常的配置方式(AddXXX)直接使用此服务。


这里偷个懒,直接用 Visual Studio 2019+.NET CORE 3.0做示例。VS 2019中有 gRPC 服务器的模板,选择后直接会创建一个现成的新手示例。


我们一定会注意到 Startup 类中 ConfigureServices 方法的语句 services.AddGrpc() 。


这个是惯例,不用去管,重点看 Configure 方法里的代码片段:


app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<GreeterService>();
});


此处和 WCF 的思想类似,将服务添加到路由终结点,让客户端连接。


然后可以看位于 Protos 文件夹下的 greet.proto 文件:


syntax = "proto3";

package Greet;

// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
string name = 1;
}

// The response message containing the greetings.
message HelloReply {
string message = 1;
}


一个最简单的rpc服务器。


然后再看 Services 文件夹下的 GreeterService.cs 文件:


using System.Threading.Tasks;
using Greet;
using Grpc.Core;

namespace GrpcService
{
public class GreeterService : Greeter.GreeterBase
{
public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
{
return Task.FromResult(new HelloReply
{
Message =$"Hello { context.Method} " + request.Name
});
}
}
}


代码的实现思路很好理解。我们可以注意到我们能够直接导入 Greet 命名空间,这是因为它已经被 Grpc.Tools 生成到了项目下 obj 文件夹的项目缓存中。


最后的一个重点在项目配置文件(*.csproj)中的 ItemGroup 节点:


<Protobuf Include="Protos\greet.proto" GrpcServices="Server" Generator="MSBuild:Compile" />


这就是在项目中引用proto文件的方法,具体细节详见官方说明:gRPC services with C# 。


然后我们可以创建个客户端尝试与服务端通讯,建立一个命令行程序,引用 Google.Protobuf、Grpc.Tools 以及 Grpc.Core 包,同时在项目配置文件中的 ItemGroup 节点中加入一句话:


<Protobuf Include="..\GrpcService\Protos\greet.proto" GrpcServices="Client" />


(我是在服务端项目同目录建立的客户端项目,所以路径直接这么写就OK)


然后我们可以直接写:


using System;
using System.Threading.Tasks;
using Greet;
using Grpc.Core;

namespace ConsoleApp1
{
class Program
{
static async Task Main(string[] args)
{
var channel = new Channel("localhost:50051", ChannelCredentials.Insecure);
var client = new Greeter.GreeterClient(channel);
var reply = await client.SayHelloAsync(
new HelloRequest { Name = "GreeterClient" });
Console.WriteLine("Greeting: " + reply.Message);
await channel.ShutdownAsync();
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}
}
}


创建频道——创建连接——发送请求——关闭频道,简单易懂。我们着重看两点。


其一是 await channel.ShutdownAsync();:


在程序退出前,最好或者说必须关闭曾经创建过的频道。


另一个就是我们会注意到此处代码中的Greeter 类所公开的接口完全是面向客户端的。


而同理,上面服务器中的 Greeter 类公开的接口则是面向服务器的,这是受项目配置中 GrpcServices=Client|Server的影响,非常智能化……


推荐阅读

(点击标题可跳转阅读)

聊一聊C#8.0中的 await foreach

C#8.0可空引用类型的使用注意要点

树莓派也跑Docker和.NET Core


看完本文有收获?请转发分享给更多人

关注「DotNet」加星标,提升.Net技能 

好文章,我在看❤️

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存